﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace dbones.PlugIn
{
    /// <summary>
    /// small plugin engine, im sure there is a better version 
    /// somewhere else
    /// </summary>
    public class TinyPlug : IPlugInObserver
    {
        ///<summary>
        /// create an instance of TinyPlug
        ///</summary>
        public TinyPlug()
        {
            //SupportedInterfaces = new List<Type>();
            LoadedPlugIns = new Dictionary<Type, List<ActualPlugIn>>();
        }

        /// <summary>
        /// list of all loaded supported interfaces and plugins
        /// </summary>
        protected Dictionary<Type, List<ActualPlugIn>> LoadedPlugIns { get; private set; }

        /// <summary>
        /// all the supported interfaces
        /// </summary>
        /// <returns>a collection of all the supported interfaces</returns>
        public virtual IEnumerable<Type> SupportedInterfaces()
        {
            return LoadedPlugIns.Keys;
        }

        /// <summary>
        /// the plugins for an interface
        /// </summary>
        /// <param name="supportedInterface">The interface</param>
        /// <returns>all the plugins for the interface</returns>
        public virtual IEnumerable<ActualPlugIn> PlugInsFor(Type supportedInterface)
        {
            if (!LoadedPlugIns.ContainsKey(supportedInterface))
            {
                throw new NotSupportedException("this interface has not been added " + supportedInterface);
            }
            return LoadedPlugIns[supportedInterface];
        }

        /// <summary>
        /// the plugins for an interface
        /// </summary>
        /// <typeparam name="T">The interface</typeparam>
        /// <returns>all the plugins for the interface</returns>
        public virtual IEnumerable<ActualPlugIn> PlugInsFor<T>()
        {
            return PlugInsFor(typeof(T));
        }

        /// <summary>
        /// add this interface as a plugin
        /// </summary>
        /// <param name="supportedInterface"></param>
        /// <returns></returns>
        public virtual TinyPlug AddSupportedInterface(Type supportedInterface)
        {
            if (!supportedInterface.IsInterface)
            {
                throw new NotSupportedException("plugin manager only supports interfaces, as the contract");
            }

            if (!LoadedPlugIns.ContainsKey(supportedInterface))
            {
                LoadedPlugIns.Add(supportedInterface, new List<ActualPlugIn>());
            }
            return this;
        }

        /// <summary>
        /// Add a supported interface
        /// </summary>
        /// <typeparam name="T">the interface</typeparam>
        public virtual TinyPlug AddSupportedInterface<T>()
        {
            return AddSupportedInterface(typeof(T));
        }

        /// <summary>
        /// see if a type has implmented any supported interfaces
        /// </summary>
        /// <param name="plugIn">plug in type. interface</param>
        /// <returns>True if this class can be supported.</returns>
        public virtual bool IsSupportedInterface(Type plugIn)
        {
            foreach (var possiblePlugIn in plugIn.GetInterfaces())
            {
                if (LoadedPlugIns.ContainsKey(possiblePlugIn))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// see if a type has implmented any supported interfaces
        /// </summary>
        /// <typeparam name="T">pass in the generic type</typeparam>
        /// <returns>true if the class supports a plugin interface</returns>
        public virtual bool IsSupportedInterface<T>()
        {
            return IsSupportedInterface(typeof(T));
        }

        #region RegisterPlugIn

        /// <summary>
        /// hook up a plug in to a supported interface
        /// </summary>
        /// <param name="supportedInterface">the supported interface</param>
        /// <param name="actualPlugIn">the plugin to register</param>
        public virtual TinyPlug RegisterPlugIn(Type supportedInterface, Type actualPlugIn)
        {
            if (supportedInterface == null) throw new ArgumentNullException("supportedInterface");
            if (actualPlugIn == null) throw new ArgumentNullException("actualPlugIn");

            if (!LoadedPlugIns.ContainsKey(supportedInterface))
            {
                throw new NotSupportedException("this interface has not been added " + supportedInterface);
            }
            if (!IsSupportedInterface(actualPlugIn))
            {
                throw new NotSupportedException("The plugin does not implement the interface " + actualPlugIn);
            }

            var ap = new ActualPlugIn(actualPlugIn);
            ap.SupportedInterface = supportedInterface;
            ap.Attach(this);
            LoadedPlugIns[supportedInterface].Add(ap);
            return this;
        }

        /// <summary>
        /// hook up a plug in to a supported interface
        /// </summary>
        /// <typeparam name="T">the supported interface</typeparam>
        /// <typeparam name="AP">the plugin to register</typeparam>
        public virtual TinyPlug RegisterPlugIn<T, AP>() where AP : T
        {
            return RegisterPlugIn(typeof(T), typeof(AP));
        }

        /// <summary>
        /// finds all plugs which support an interface
        /// </summary>
        /// <typeparam name="T">The interface</typeparam>
        /// <param name="assembly">an assembly to search in, will extract the exported classes.</param>
        public virtual TinyPlug RegisterPlugIn<T>(Assembly assembly)
        {
            if (assembly == null) throw new ArgumentNullException("assembly");
            IEnumerable<Type> types = assembly.GetExportedTypes().Where(x => x.GetInterfaces().Contains(typeof(T)));
            RegisterPlugIn<T>(types);
            return this;
        }

        /// <summary>
        /// to to register any types which support the required interface
        /// </summary>
        /// <typeparam name="T">the interface which you want to register the plugins for</typeparam>
        /// <param name="types">a container of possible plugins</param>
        public virtual TinyPlug RegisterPlugIn<T>(IEnumerable<Type> types)
        {
            if (types == null) throw new ArgumentNullException("types");
            foreach (var possiblePlugin in types)
            {
                RegisterPlugIn(typeof(T), possiblePlugin);
            }
            return this;
        }

        #endregion

        #region RegisterPlugInToAny

        /// <summary>
        /// hook up a plug in to any supported interfaces (could be very slow)
        /// </summary>
        /// <param name="actualPlugIn">the plugin to register</param>
        public virtual TinyPlug RegisterPlugInToAny(Type actualPlugIn)
        {
            if (actualPlugIn == null) throw new ArgumentNullException("actualPlugIn");

            if (!IsSupportedInterface(actualPlugIn))
            {
                //not supported
                return this;
                //throw new NotSupportedException("The plugin does not implement the interface " + actualPlugIn);
            }

            foreach (var supportedInterface in
                actualPlugIn.GetInterfaces()
                .Where(x => LoadedPlugIns.Keys.Contains(x)))
            {
                var ap = new ActualPlugIn(actualPlugIn);
                ap.SupportedInterface = supportedInterface;
                ap.Attach(this);
                LoadedPlugIns[supportedInterface].Add(ap);
            }
            return this;
        }

        /// <summary>
        /// hook up a plug in to any supported interfaces (could be very slow)
        /// </summary>
        /// <param name="assembly">an assembly which contains many plugins, looks for the public types</param>
        public virtual TinyPlug RegisterPlugInToAny(Assembly assembly)
        {
            if (assembly == null) throw new ArgumentNullException("assembly");
            IEnumerable<Type> types = assembly.GetExportedTypes().Where(x => x.IsClass);
            RegisterPlugInToAny(types);
            return this;
        }

        /// <summary>
        /// this trys to register any types to any of the supported interfaces
        /// </summary>
        /// <param name="types">the container of types</param>
        public virtual TinyPlug RegisterPlugInToAny(IEnumerable<Type> types)
        {
            if (types == null) throw new ArgumentNullException("types");
            foreach (var possiblePlugIn in types)
            {
                RegisterPlugInToAny(possiblePlugIn);
            }
            return this;
        }

        #endregion

        /// <summary>
        /// Remove a plugin
        /// </summary>
        /// <param name="plugIn">To detach/Remove</param>
        public virtual TinyPlug UnregisterPlugIn(ActualPlugIn plugIn)
        {
            if (plugIn == null) throw new ArgumentNullException("plugIn");
            LoadedPlugIns[plugIn.SupportedInterface].Remove(plugIn);
            plugIn.Detach(this);
            return this;
        }

        /// <summary>
        /// Create an instance of the object
        /// </summary>
        /// <typeparam name="T">The type of plugin, the supported interface</typeparam>
        /// <param name="plugIn">The implementation of the plugin</param>
        /// <returns>a created instance of this class.</returns>
        public T CreateInstance<T>(ActualPlugIn plugIn) where T : class
        {
            if (plugIn == null) throw new ArgumentNullException("plugIn");
            if (!LoadedPlugIns.ContainsKey(typeof(T)))
            {
                throw new NotSupportedException("Does not support the interface " + typeof(T));
            }
            return (T)Activator.CreateInstance(plugIn.Class);
        }

        /// <summary>
        /// Create an instance of the object, using the 'Default' ActualPlugIn
        /// </summary>
        /// <typeparam name="T">The type of plugin, the supported interface</typeparam>
        /// <returns>a created instance of this class.</returns>
        public T CreateInstance<T>() where T : class
        {
            Type t = typeof(T);
            if (!LoadedPlugIns.ContainsKey(typeof(T)))
            {
                throw new NotSupportedException("Does not support the interface " + typeof(T));
            }

            ActualPlugIn plugIn;
            plugIn =
                LoadedPlugIns[t].Count == 1
                ? LoadedPlugIns[t][0]
                : LoadedPlugIns[t].Where(x => x.IsDefault).First();

            return plugIn == null
                ? null
                : (T)Activator.CreateInstance(plugIn.Class);
        }

        /// <summary>
        /// used to update the list of plugins, so only one can be the default
        /// </summary>
        /// <param name="plugIn">the plugin which has a change</param>
        public void Update(ActualPlugIn plugIn)
        {
            if (plugIn == null) throw new ArgumentNullException("plugIn");
            //check to see if this should now be the default implementation
            if (plugIn.IsDefault)
            {
                foreach (var other in LoadedPlugIns[plugIn.SupportedInterface]
                    .Where(x => x != plugIn))
                {
                    other.IsDefault = false;
                }
            }
        }
    }
}